﻿// SKNotes - a note taking and gathering tool
// Copyright (C) 2009  Stefan Kueng
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

namespace SKNotes
{
    using System;
    using System.Collections;
    using System.Collections.Specialized;
    using System.Diagnostics;
    using System.IO;
    using System.Net;
    using System.Text.RegularExpressions;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Input;
    using ICSharpCode.SharpZipLib.Zip;
    using IfacesEnumsStructsClasses;
    using SKNotes.AsyncProtocolHandler;
    using SKNotes.CustomControls.HtmlEditor.Control;
    using SKNotes.Utilities;
    using SKNotes.Utilities.Events;

    /// <summary>
    /// Interaction logic for NoteViewer.xaml
    /// </summary>
    public partial class NoteViewer : UserControl
    {
        private Note note;
        private DeferredAction deferredAction;
        private readonly object noteLock = new object();

        public NoteViewer()
        {
            DataContext = this.note;
            InitializeComponent();
            PluggableProtocolRegistrationServices.RegisterTemporaryProtocolHandler<NotesProtocol>(NotesProtocol.Schema);
        }

        private void webbrowser_Unloaded(object sender, RoutedEventArgs e)
        {
            deferredAction.Dispose();
            deferredAction = null;
            if (noteview.IsHTMLChanged == true)
            {
                this.SaveNote();
                noteview.IsHTMLChanged = false;
            }
            PluggableProtocolRegistrationServices.UnregisterTemporaryProtocolHandler<NotesProtocol>(NotesProtocol.Schema);
        }

        private void menu_FontChanged(object sender, FontSelectionEventArgs e)
        {
            if (e.Font != null)
                noteview.ExecuteCommand("FontName", e.Font.ToString());
        }

        private void menu_FontSizeChanged(object sender, FontSelectionEventArgs e)
        {
            if (e.FontSize > 0)
                noteview.ExecuteCommand("FontSize", e.FontSize);
        }

        private void menu_FontColourSelected(object sender, RoutedEventArgs e)
        {
            string colourString = string.Format("{0:X2}{1:X2}{2:X2}",
               this.ccMenu.FontColour.R, this.ccMenu.FontColour.G, this.ccMenu.FontColour.B);
            noteview.ExecuteCommand("ForeColor", colourString);
        }

        private void menu_HighlightColourChanged(object sender, RoutedEventArgs e)
        {
            string colourString = string.Format("{0:X2}{1:X2}{2:X2}",
                this.ccMenu.HighlightColour.R, this.ccMenu.HighlightColour.G, this.ccMenu.HighlightColour.B);
            noteview.ExecuteCommand("BackColor", colourString);
        }

        private void menu_AnchorInserted(object sender, AnchorEventArgs e)
        {
            if (!noteview.IsDocumentLoaded) return;

            string linkHtml = string.Format("<a href=\"{0:0}\">{1:0}</a>", e.Url, e.LinkText);
            noteview.InsertHtml(linkHtml);
        }

        private void menu_AnchorInserting(object sender, AnchorEventArgs e)
        {
            if (!noteview.IsDocumentLoaded) return;

            e.LinkText = noteview.GetSelectedText(false);
        }

        private void menu_StrikeThoughSelected(object sender, RoutedEventArgs e)
        {
            noteview.ExecuteCommand("StrikeThrough");
        }

        private void cutCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            noteview.ExecuteCommand("Cut");
        }

        private void copyCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            noteview.ExecuteCommand("Copy");
        }

        private void pasteCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            noteview.ExecuteCommand("Paste");
        }

        private void boldCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            noteview.ExecuteCommand("Bold");
        }

        private void italicCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            noteview.ExecuteCommand("Italic");
        }

        private void underlineCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            noteview.ExecuteCommand("Underline");
        }

        private void listOlCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            noteview.ExecuteCommand("InsertOrderedList");
        }

        private void listUlCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            noteview.ExecuteCommand("InsertUnorderedList");
        }

        private void alignLeftCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            noteview.ExecuteCommand("JustifyLeft");
        }

        private void alignCentreCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            noteview.ExecuteCommand("JustifyCenter");
        }

        private void alignRightCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            noteview.ExecuteCommand("JustifyRight");
        }

        private void indentCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            noteview.ExecuteCommand("Indent");
        }

        private void outdentCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            noteview.ExecuteCommand("Outdent");
        }

        private void undoCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            noteview.ExecuteCommand("Undo");
        }

        private void redoCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            noteview.ExecuteCommand("Redo");
        }

        private void cmd_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = (this.IsEnabled && noteview.IsDocumentLoaded);
        }

        private void removeTag_Click(object sender, RoutedEventArgs e)
        {
            if (this.note != null)
            {
                Button btn = sender as Button;
                if (btn != null)
                {
                    string tagsstring = btn.CommandParameter as string;
                    if (tagsstring != null)
                    {
                        NoteTag tagToRemove = new NoteTag(tagsstring, this.note.NoteBook);
                        this.note.RemoveTag(tagToRemove);
                    }
                }
            }
        }

        public void SetFocus()
        {
            noteview.SetFocus();
        }

        public Note Note
        {
            set
            {
                if (deferredAction != null)
                {
                    deferredAction.Dispose();
                    deferredAction = null;
                    if (noteview.IsHTMLChanged == true)
                    {
                        this.SaveNote();
                        noteview.IsHTMLChanged = false;
                    }
                }
                lock (noteLock)
                {
                    this.note = value;
                    if (value != null)
                    {
                        noteview.Navigate(this.note.Document);
                    }
                    else
                    {
                        noteview.Navigate();
                    }
                }
                DataContext = this.note;
            }
        }

        private void CommandExecuteIfListItemSelected(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = (attachmentList.SelectedItem != null);
        }

        private void DeleteAttachment(object sender, ExecutedRoutedEventArgs e)
        {
            string attachment = attachmentList.SelectedItem as string;
            if (attachment != null)
            {
                lock (noteLock)
                {
                    this.note.RemoveAttachment(attachment);
                }
            }
        }

        private void OpenAttachment(object sender, ExecutedRoutedEventArgs e)
        {
            string attachment = attachmentList.SelectedItem as string;
            if (attachment != null)
            {
                string tempFile = new TempFile(Path.GetRandomFileName() + Path.GetExtension(attachment)).Path;
                lock (noteLock)
                {
                    this.note.SaveAttachment(attachment, tempFile);
                }
                
                // spawn new process
                System.Diagnostics.Process p = new System.Diagnostics.Process();
                p.StartInfo.RedirectStandardOutput = false;
                p.StartInfo.Verb = "Open";
                p.StartInfo.FileName = tempFile;
                p.StartInfo.UseShellExecute = true;
                p.Start();
            }
        }

        private void SaveAttachment(object sender, ExecutedRoutedEventArgs e)
        {
            string attachment = attachmentList.SelectedItem as string;
            if (attachment != null)
            {
                // Configure save file dialog box
                Microsoft.Win32.SaveFileDialog dlg = new Microsoft.Win32.SaveFileDialog();
                dlg.FileName = attachment; // Default file name
                dlg.Filter = "All Files (.*)|*.*"; // Filter files by extension

                // Show save file dialog box
                Nullable<bool> result = dlg.ShowDialog();

                // Process save file dialog box results
                if (result == true)
                {
                    lock (noteLock)
                    {
                        // Save document
                        this.note.SaveAttachment(attachment, dlg.FileName);
                    }
                }
            }
        }

        private void SaveNote()
        {
            lock (noteLock)
            {
                string htmlText = noteview.GetText(true);
                if (this.note == null)
                {
                    if ((htmlText == null) || (htmlText.Length == 0))
                        return;
                    if (htmlText.ToLower().Contains("<body></body>"))
                        return; // empty body -> empty note, nothing to save
                }

                if (this.note == null)
                {
                    // check if we have a notebook
                    if (NoteList.Instance.CurrentNotebook == null)
                    {
                        if (NoteList.Instance.Tags.Count == 0)
                        {
                            // no notebook exists yet, create a new default one
                            NoteList.Instance.CreateNotebook("notebook");
                        }
                        else
                        {
                            NoteList.Instance.CurrentNotebook = NoteList.Instance.Tags[0];
                        }
                    }
                    // create a new note
                    this.note = new Note();
                    NoteList.Instance.AddNewNote(this.note);
                }
                if (this.note.Name == null || this.note.Name.Length == 0)
                {
                    // try to get a title for the new note from the entered text
                    StringReader sr = new StringReader(noteview.GetText(false));
                    string line;
                    string title = null;
                    while ((title == null) && ((line = sr.ReadLine()) != null))
                    {
                        if (line.Length > 0)
                        {
                            int sentenceEnd = line.IndexOfAny(new char[] { '.', '!', '?' });
                            if (sentenceEnd > 0)
                                title = line.Substring(0, sentenceEnd);
                            else
                                title = line;
                            title = title.Trim();
                            if (title.Length == 0)
                                title = null;
                        }
                    }

                    if ((title == null) || (title.Length == 0))
                    {
                        // for some reason, we don't have a title!
                        // use a default title
                        title = "new note";
                    }
                    this.note.Name = title.Trim();
                }
                string sknotesHost = Path.GetFileName(this.note.Path);
                NameValueCollection externalFileCollection = new NameValueCollection();
                ConvertRemoteLinksToLocalLinks(sknotesHost, ref htmlText, ref externalFileCollection);
                this.note.Document = htmlText;

                // now remove all inline files in the note which are not referenced anymore in the html text
                // TODO: if the user uses Ctrl-X (Cut) to remove part of the note but then decides to
                // paste that part into another note, that other note won't be able to get the inline files
                // anymore!
                StringCollection inlineFilesToRemove = new StringCollection();
                foreach (string fileUrl in this.note.InlineFiles)
                {
                    try
                    {
                        string url = externalFileCollection["\"sknotes://" + sknotesHost + "/" + fileUrl + "\""];
                        if (string.IsNullOrEmpty(url))
                        {
                            inlineFilesToRemove.Add(fileUrl);
                        }
                    }
                    catch (System.ArgumentOutOfRangeException)
                    {
                        inlineFilesToRemove.Add(fileUrl);
                    }
                }
                foreach (string inlineFile in inlineFilesToRemove)
                {
                    this.note.RemoveInlineFile(inlineFile);
                }

                // add all references to the notes inline file collection
                foreach (string DelimitedFileUrl in externalFileCollection.AllKeys)
                {
                    string fileUrl = externalFileCollection[DelimitedFileUrl];

                    if (!fileUrl.StartsWith("sknotes://"+note.ID))
                    {
                        string NewPath = fileUrl.Replace("/", "");
                        NewPath = NewPath.Replace(":", "");
                        NewPath = NewPath.Replace("?", "");
                        NewPath = NewPath.Replace("%", "");

                        string url = DelimitedFileUrl.Trim('\"');
                        if (fileUrl.StartsWith("sknotes://"))
                        {
                            string host = fileUrl.Substring(10);
                            host = host.Substring(0, host.IndexOf('/'));
                            // get the file from the note itself
                            string zipFilePath = NoteList.Instance.CurrentNotebook.Path + "\\" + host;
                            string segment = fileUrl.Substring(10);
                            segment = segment.Substring(segment.IndexOf('/') + 1);
                            if (File.Exists(zipFilePath))
                            {
                                TempFile tempFile = new TempFile();
                                FileStream output = new FileStream(tempFile.Path, FileMode.Create);
                                using (ZipInputStream s = new ZipInputStream(File.OpenRead(zipFilePath)))
                                {
                                    ZipEntry theEntry;
                                    while ((theEntry = s.GetNextEntry()) != null)
                                    {
                                        if ((theEntry.IsFile) && (theEntry.Name == "inlines/" + segment))
                                        {
                                            // send the data
                                            byte[] data = new byte[4096];
                                            int size = s.Read(data, 0, data.Length);
                                            while (size > 0)
                                            {
                                                output.Write(data, 0, size);
                                                size = s.Read(data, 0, data.Length);
                                            }
                                            break;
                                        }
                                    }

                                    // Close can be omitted as the using statement will do it automatically
                                    // but leaving it here reminds you that is should be done.
                                    s.Close();
                                }

                                output.Close();
                                this.note.AddInlineFile(tempFile.Path, NewPath);
                            }

                        }
                        ArrayList cacheList = WinApis.FindUrlCacheEntries(url);
                        bool bFoundInCache = false;
                        foreach (INTERNET_CACHE_ENTRY_INFO cacheEntry in cacheList)
                        {
                            if (cacheEntry.lpszLocalFileName.Length != 0)
                            {
                                this.note.AddInlineFile(cacheEntry.lpszLocalFileName, NewPath);
                                bFoundInCache = true;
                                break;
                            }
                        }

                        if (!bFoundInCache)
                        {
                            WebClient client = new WebClient();
                            TempFile tempFile = new TempFile();
                            try
                            {
                                client.DownloadFile(url, tempFile.Path);
                                this.note.AddInlineFile(tempFile.Path, NewPath);
                            }
                            catch (System.Exception e)
                            {
                                Debug.Print(e.Message);
                            }
                        }
                    }
                }

                this.note.Save();
            }
        }

        private void ConvertRemoteLinksToLocalLinks(string host, ref string html, ref NameValueCollection externalFileCollection)
        {
            // scan the html for inline links like <img src=...> and
            // convert those urls from http:// to sknotes://, get the file/content
            // that url points to into a file and add it to the inline collection of the
            // note.

            // but first sanitize the html a little bit
            html = HTMLHelpers.StripHtmlTag("script", html);
            html = HTMLHelpers.StripHtmlTag("iframe", html);

            // find the base for the urls, if there is any
            string BaseUrlFolder = Regex.Match(html, "<base[^>]+?href=['\"]{0,1}(?<BaseUrl>[^'\">]+)['\"]{0,1}", RegexOptions.IgnoreCase).Groups["BaseUrl"].Value;

            if (!string.IsNullOrEmpty(BaseUrlFolder))
            {
                if (BaseUrlFolder.EndsWith("/"))
                {
                    BaseUrlFolder = BaseUrlFolder.Substring(0, BaseUrlFolder.Length - 1);
                }
            }
            // remove the <base href=''> tag if present.
            html = Regex.Replace(html, "<base[^>]*?>", "");

            Regex r = default(Regex);

            // src='filename.ext' ; background="filename.ext"
            // note that we have to test 3 times to catch all quote styles: '', "", and none
            r =
                new Regex(
                    "(\\ssrc|\\sbackground)\\s*=\\s*((?<Key>'(?<Value>[^']+)')|(?<Key>\"(?<Value>[^\"]+)\")|(?<Key>(?<Value>[^ \\n\\r\\f]+)))",
                    RegexOptions.IgnoreCase | RegexOptions.Multiline);
            AddMatchesToCollection(html, r, ref externalFileCollection);

            // @import "style.css" or @import url(style.css)
            r =
                new Regex(
                    "(@import\\s|\\S+-image:|background:)\\s*?(url)*\\s*?(?<Key>[\"'(]{1,2}(?<Value>[^\"')]+)[\"')]{1,2})",
                    RegexOptions.IgnoreCase | RegexOptions.Multiline);
            AddMatchesToCollection(html, r, ref externalFileCollection);

            // <link rel=stylesheet href="style.css">
            r = new Regex("<link[^>]+?href\\s*=\\s*(?<Key>('|\")*(?<Value>[^'\">]+)('|\")*)",
                          RegexOptions.IgnoreCase | RegexOptions.Multiline);
            AddMatchesToCollection(html, r, ref externalFileCollection);

            if (externalFileCollection.Count == 0)
                return;

            foreach (string DelimitedFileUrl in externalFileCollection.AllKeys)
            {
                string FileUrl = externalFileCollection[DelimitedFileUrl];

                if (!FileUrl.StartsWith("sknotes://"+note.ID))
                {
                    string NewPath = FileUrl.Replace("/", "");
                    NewPath = NewPath.Replace(":", "");
                    NewPath = NewPath.Replace("?", "");
                    NewPath = NewPath.Replace("%", "");
                    string DelimitedReplacement = Regex.Replace(DelimitedFileUrl,
                                                                "^(?<StartDelim>\"|'|\\()*(?<Value>[^'\"]*)(?<EndDelim>\"|'|\\))*$",
                                                                "${StartDelim}sknotes://"+ host + "/" + NewPath + "${EndDelim}");

                    // correct original Url references in Html so they point to our local files
                    html = html.Replace(DelimitedFileUrl, DelimitedReplacement);
                }
            }
        }

        private void AddMatchesToCollection(string html, Regex r, ref NameValueCollection nvc)
        {
            string key = null;
            string value = null;

            bool headerDisplayed = false;
            Regex urlRegex = new Regex("(^https*://[A-Za-z0-9%()]+)|(^file:///*[A-Za-z0-9%()]+)|(^ftp://[A-Za-z0-9%()]+)|(^sknotes://[A-Za-z0-9%()]+)", RegexOptions.IgnoreCase);

            foreach (Match m in r.Matches(html))
            {
                if (!headerDisplayed)
                {
                    Debug.WriteLine("Matches added from regex:");
                    Debug.WriteLine("\"" + r + "\"");

                    headerDisplayed = true;
                }

                key = m.Groups["Key"].ToString();
                value = m.Groups["Value"].ToString();

                if (nvc[key] == null)
                {
                    Debug.WriteLine(" Match: " + m);
                    Debug.WriteLine("   Key: " + key);
                    Debug.WriteLine(" Value: " + value);

                    if (!urlRegex.IsMatch(value))
                    {
                        Debug.WriteLine("Match discarded; does not appear to be valid fully qualified http:// Url",
                                        "Error");
                    }
                    else
                    {
                        nvc.Add(key, value);
                    }
                }
            }
        }

        private void noteview_HtmlChanged(object sender, RoutedEventArgs e)
        {
            DeferSaveNote();
        }

        private void DeferSaveNote()
        {
            if (this.deferredAction == null)
            {
                this.deferredAction = DeferredAction.Create(() => this.SaveNote());
            }

            this.deferredAction.Defer(new TimeSpan(0, 0, 2));
        }

        private void noteview_BeforeNavigate2(object sender, BeforeNavigate2EventArgs e)
        {
            if (e.url != null)
            {
                if ((e.url.Length > 0) && (e.url.CompareTo("about:blank") != 0))
                    e.Handled = true;
            }
        }

        private void noteview_DragEnter(object sender, DragEnterEventArgs e)
        {
            e.dropeffect = DROPEFFECT.COPY;
            e.Handled = true;
        }

        private void noteview_DragOver(object sender, DragOverEventArgs e)
        {
            e.dropeffect = DROPEFFECT.COPY;
            e.Handled = true;
        }

        private void noteview_Drop(object sender, DropEventArgs e)
        {
            if (e.dataobject.GetDataPresent(DataFormats.FileDrop))
            {
                string[] fileNames = e.dataobject.GetData(DataFormats.FileDrop, true) as string[];
                foreach (string fileName in fileNames)
                {
                    if ((fileName.ToLower().EndsWith(".jpg")) ||
                        (fileName.ToLower().EndsWith(".png")) ||
                        (fileName.ToLower().EndsWith(".gif")))
                    {
                        string imgTag = "<img src=\"" + fileName + "\"><br/>";
                        noteview.InsertHtml(imgTag);
                    }
                    else
                    {
                        lock (noteLock)
                        {
                            note.Save();
                            // add file as attachment
                            note.AddAttachment(fileName);
                        }
                    }
                }
            }
        }

        private void noteview_BeforePaste(object sender, RoutedEventArgs e)
        {
            if (Clipboard.ContainsData("HTML Format"))
            {
                string htmlText = (string)Clipboard.GetData("HTML Format");
                // extract the source url
                string sourceUrl = Regex.Match(htmlText, "^SourceURL:(?<SourceUrl>.+)$", RegexOptions.IgnoreCase | RegexOptions.Multiline).Groups["SourceUrl"].Value;
                sourceUrl = sourceUrl.Replace("\r", "");
                sourceUrl = sourceUrl.Replace("\n", "");
                if (note == null)
                {
                    this.note = new Note();
                    NoteList.Instance.AddNewNote(this.note);
                    this.DeferSaveNote();
                }
                if (sourceUrl != "about:blank")
                {
                    note.SourceURL = sourceUrl;
                }
                // sanitize the html
                htmlText = HTMLHelpers.StripHtmlTag("script", htmlText);
                htmlText = HTMLHelpers.StripHtmlTag("iframe", htmlText);

                Clipboard.SetData("HTML Format", htmlText);
            }
        }
    }
}
